/*=========================================================================
 *
 *  Copyright Insight Software Consortium
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0.txt
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 *=========================================================================*/
#ifndef itkDCMTKFileReader_h
#define itkDCMTKFileReader_h

#include "ITKIODCMTKExport.h"
#include <stack>
#include <vector>
#include "itkByteSwapper.h"
#include "itkIntTypes.h"
#include "vnl/vnl_vector.h"
#include "dcmtk/dcmdata/dcdict.h"             // For DcmDataDictionary
#include "dcmtk/dcmdata/dcdicent.h"
#include "dcmtk/dcmdata/dcxfer.h"
#include "dcmtk/dcmdata/dcvrds.h"
#include "dcmtk/dcmdata/dcstack.h"
#include "dcmtk/dcmdata/dcdatset.h"
#include "dcmtk/dcmdata/dcitem.h"
#include "dcmtk/dcmdata/dcvrobow.h"
#include "dcmtk/dcmdata/dcsequen.h"
#include "itkMacro.h"
#include "itkImageIOBase.h"

class DcmSequenceOfItems;
class DcmFileFormat;
class DcmDictEntry;

// Don't print error messages if you're not throwing
// an exception
//     std::cerr body;
#define DCMTKExceptionOrErrorReturn(body)       \
  {                                             \
    if(throwException)                          \
      {                                         \
      itkGenericExceptionMacro(body);           \
      }                                         \
    else                                        \
      {                                         \
      return EXIT_FAILURE;                      \
      }                                         \
  }

namespace itk
{
// Forward reference because of circular dependencies
class ITK_FORWARD_EXPORT DCMTKSequence;

class ITKIODCMTK_EXPORT DCMTKItem
{
public:
  DCMTKItem() : m_DcmItem(nullptr)
    {
    }
  void SetDcmItem(DcmItem *item);
  int GetElementSQ(const unsigned short group,
                   const unsigned short entry,
                   DCMTKSequence &sequence,
                   const bool throwException = true) const;

private:
  DcmItem *m_DcmItem;
};

class ITKIODCMTK_EXPORT DCMTKSequence
{
public:
  DCMTKSequence() : m_DcmSequenceOfItems(nullptr) {}
  void SetDcmSequenceOfItems(DcmSequenceOfItems *seq);
  int card() const;
  int GetSequence(unsigned long index,
                  DCMTKSequence &target,const bool throwException = true) const;
  int GetStack(const unsigned short group,
               const unsigned short element,
               DcmStack &resultStack, const bool throwException = true) const;
  int GetElementCS(const unsigned short group,
                   const unsigned short element,
                   std::string &target,
                   const bool throwException = true) const;

  int GetElementOB(const unsigned short group,
                   const unsigned short element,
                   std::string &target,
                   const bool throwException = true) const;

  int GetElementCSorOB(const unsigned short group,
                       const unsigned short element,
                       std::string &target,
                       const bool throwException = true) const;

  template <typename TType>
  int  GetElementDSorOB(const unsigned short group,
                        const unsigned short element,
                        TType  &target,
                        const bool throwException = true) const
    {
      if(this->GetElementDS<TType>(group,element,1,&target,false) == EXIT_SUCCESS)
        {
        return EXIT_SUCCESS;
        }
      std::string val;
      if(this->GetElementOB(group,element,val,throwException) != EXIT_SUCCESS)
        {
        DCMTKExceptionOrErrorReturn(<< "Cant find DecimalString element " << std::hex
                       << group << " " << std::hex
                       << element << std::dec);
        }
      const char *data = val.c_str();
      const auto * fptr = reinterpret_cast<const TType *>(data);
      target = *fptr;
      return EXIT_SUCCESS;

    }

  template <typename TType>
  int  GetElementDSorOB(const unsigned short group,
                        const unsigned short element,
                        int count,
                        TType  *target,
                        const bool throwException = true) const
    {
      if(this->GetElementDS<TType>(group,element,count,target,false) == EXIT_SUCCESS)
        {
        return EXIT_SUCCESS;
        }
      std::string val;
      if(this->GetElementOB(group,element,val,throwException) != EXIT_SUCCESS)
        {
        DCMTKExceptionOrErrorReturn(<< "Cant find DecimalString element " << std::hex
                       << group << " " << std::hex
                       << element << std::dec);
        }
      const char *data = val.c_str();
      const auto * fptr = reinterpret_cast<const TType *>(data);
      for(int i = 0; i < count; ++i)
        {
        target[i] = fptr[i];
        }
      return EXIT_SUCCESS;
    }


  int GetElementFD(const unsigned short group,
                   const unsigned short element,
                   int count,
                   double *target,
                   const bool throwException = true) const;
  int GetElementFD(const unsigned short group,
                   const unsigned short element,
                   double &target,
                   const bool throwException = true) const;
  int GetElementDS(const unsigned short group,
                   const unsigned short element,
                   std::string &target,
                   const bool throwException = true) const;
  int GetElementTM(const unsigned short group,
                   const unsigned short element,
                   std::string &target,
                   const bool throwException = true) const;
  /** Get an array of data values, as contained in a DICOM
    * DecimalString Item
    */
  template <typename TType>
  int GetElementDS(const unsigned short group,
                   const unsigned short element,
                   unsigned short count,
                   TType  *target,
                   const bool throwException = true) const
    {
      DcmStack resultStack;
      if(this->GetStack(group,element,resultStack,throwException) != EXIT_SUCCESS)
        {
        return EXIT_FAILURE;
        }
      auto * dsItem = dynamic_cast<DcmDecimalString *>(resultStack.top());
      if(dsItem == nullptr)
        {
        DCMTKExceptionOrErrorReturn(<< "Can't get DecimalString Element at tag "
                       << std::hex << group << " "
                       << element << std::dec);
        }

      OFVector<Float64> doubleVals;
      if(dsItem->getFloat64Vector(doubleVals) != EC_Normal)
        {
        DCMTKExceptionOrErrorReturn(<< "Cant extract Array from DecimalString " << std::hex
                       << group << " " << std::hex
                       << element << std::dec);
        }
      if(doubleVals.size() != count)
        {
        DCMTKExceptionOrErrorReturn(<< "DecimalString " << std::hex
                       << group << " " << std::hex
                       << element << " expected "
                       << count << "items, but found "
                       << doubleVals.size() << std::dec);

        }
      for(unsigned i = 0; i < count; i++)
        {
        target[i] = static_cast<TType>(doubleVals[i]);
        }
      return EXIT_SUCCESS;
    }
  int GetElementSQ(const unsigned short group,
                   const unsigned short element,
                   DCMTKSequence &target,
                   const bool throwException = true) const;
  int GetElementItem(unsigned short itemIndex,
                     DCMTKItem &target,
                     const bool throwException = true) const;

  void print(std::ostream &out) const
    {
      this->m_DcmSequenceOfItems->print(out);
    }
private:
  DcmSequenceOfItems *m_DcmSequenceOfItems;
};

class ITKIODCMTK_EXPORT DCMTKFileReader
{
public:
  using Self = DCMTKFileReader;

  DCMTKFileReader() : m_DFile(nullptr),
                      m_Dataset(nullptr),
                      m_Xfer(EXS_Unknown),
                      m_FrameCount(0),
                      m_FileNumber(-1L)
    {}
  ~DCMTKFileReader();

  void SetFileName(const std::string &fileName);

  const std::string &GetFileName() const;

  void LoadFile();

  int GetElementLO(const unsigned short group,
                   const unsigned short element,
                   std::string &target,
                   const bool throwException = true) const;
  int GetElementLO(const unsigned short group,
                   const unsigned short element,
                   std::vector<std::string> &target,
                   const bool throwException = true) const;

  /** Get an array of data values, as contained in a DICOM
    * DecimalString Item
    */
  template <typename TType>
  int  GetElementDS(const unsigned short group,
                    const unsigned short element,
                    unsigned short count,
                    TType  *target,
                    const bool throwException = true) const
    {
      DcmTagKey tagkey(group,element);
      DcmElement *el;
      if(this->m_Dataset->findAndGetElement(tagkey,el) != EC_Normal)
        {
        DCMTKExceptionOrErrorReturn(<< "Cant find tag " << std::hex
                       << group << " " << std::hex
                       << element << std::dec);
        }
      auto * dsItem = dynamic_cast<DcmDecimalString *>(el);
      if(dsItem == nullptr)
        {
        DCMTKExceptionOrErrorReturn(<< "Cant find DecimalString element " << std::hex
                       << group << " " << std::hex
                       << element << std::dec);
        }
      OFVector<Float64> doubleVals;
      if(dsItem->getFloat64Vector(doubleVals) != EC_Normal)
        {
        DCMTKExceptionOrErrorReturn(<< "Cant extract Array from DecimalString " << std::hex
                       << group << " " << std::hex
                       << element << std::dec);
        }
      if(doubleVals.size() != count)
        {
        DCMTKExceptionOrErrorReturn(<< "DecimalString " << std::hex
                       << group << " " << std::hex
                       << element << " expected "
                       << count << "items, but found "
                       << doubleVals.size() << std::dec);

        }
      for(unsigned i = 0; i < count; i++)
        {
        target[i] = static_cast<TType>(doubleVals[i]);
        }
      return EXIT_SUCCESS;
    }

  template <typename TType>
  int  GetElementDSorOB(const unsigned short group,
                        const unsigned short element,
                        TType  &target,
                        const bool throwException = true) const
    {
      if(this->GetElementDS<TType>(group,element,1,&target,false) == EXIT_SUCCESS)
        {
        return EXIT_SUCCESS;
        }
      std::string val;
      if(this->GetElementOB(group,element,val,throwException) != EXIT_SUCCESS)
        {
        DCMTKExceptionOrErrorReturn(<< "Cant find DecimalString element " << std::hex
                       << group << " " << std::hex
                       << element << std::dec);
        }
      const char *data = val.c_str();
      const auto * fptr = reinterpret_cast<const TType *>(data);
      target = *fptr;
      switch(this->GetTransferSyntax())
        {
        case EXS_LittleEndianImplicit:
        case EXS_LittleEndianExplicit:
          itk::ByteSwapper<TType>::SwapFromSystemToLittleEndian(&target);
          break;
        case EXS_BigEndianImplicit:
        case EXS_BigEndianExplicit:
          itk::ByteSwapper<TType>::SwapFromSystemToBigEndian(&target);
          break;
        default:
          break;
        }
      return EXIT_SUCCESS;

    }
  /** Get a DecimalString Item as a single string
   */
  int  GetElementDS(const unsigned short group,
                    const unsigned short element,
                    std::string &target,
                    const bool throwException = true) const;
  int  GetElementFD(const unsigned short group,
                    const unsigned short element,
                    double &target,
                    const bool throwException = true) const;
  int  GetElementFD(const unsigned short group,
                    const unsigned short element,
                    int count,
                    double * target,
                    const bool throwException = true) const;
  int  GetElementFL(const unsigned short group,
                    const unsigned short element,
                    float &target,
                    const bool throwException = true) const;
  int  GetElementFLorOB(const unsigned short group,
                        const unsigned short element,
                        float &target,
                        const bool throwException = true) const;

  int  GetElementUS(const unsigned short group,
                    const unsigned short element,
                    unsigned short &target,
                    const bool throwException = true) const;
  int  GetElementUS(const unsigned short group,
                    const unsigned short element,
                    unsigned short *&target,
                    const bool throwException = true) const;
  /** Get a DecimalString Item as a single string
   */
  int  GetElementCS(const unsigned short group,
                    const unsigned short element,
                    std::string &target,
                    const bool throwException = true) const;

  /** Get a PersonName Item as a single string
   */
  int  GetElementPN(const unsigned short group,
                    const unsigned short element,
                    std::string &target,
                    const bool throwException = true) const;

  /** get an IS (Integer String Item
   */
  int  GetElementIS(const unsigned short group,
                    const unsigned short element,
                    ::itk::int32_t  &target,
                    const bool throwException = true) const;

  int  GetElementSL(const unsigned short group,
                    const unsigned short element,
                    ::itk::int32_t  &target,
                    const bool throwException = true) const;

  int  GetElementISorOB(const unsigned short group,
                        const unsigned short element,
                        ::itk::int32_t  &target,
                        const bool throwException = true) const;

  int  GetElementCSorOB(const unsigned short group,
                        const unsigned short element,
                        std::string &target,
                        const bool throwException = true) const;

  /** get an OB OtherByte Item
   */
  int  GetElementOB(const unsigned short group,
                    const unsigned short element,
                    std::string &target,
                    const bool throwException = true) const;

  int GetElementSQ(const unsigned short group,
                   unsigned short entry,
                   DCMTKSequence &sequence,
                   const bool throwException = true) const;

  int GetElementUI(const unsigned short group,
                  unsigned short entry,
                  std::string &target,
                   const bool throwException = true) const;

  int GetElementDA(const unsigned short group,
                   const unsigned short element,
                   std::string &target,
                   const bool throwException = true) const;

  int GetElementTM(const unsigned short group,
                   const unsigned short element,
                   std::string &target,
                   const bool throwException = true) const;

  int GetDirCosines(vnl_vector<double> &dir1,
                    vnl_vector<double> &dir2,
                    vnl_vector<double> &dir3) const;

  int GetDirCosArray(double * const dircos) const;

  int GetFrameCount() const;

  int GetSlopeIntercept(double &slope, double &intercept) const;

  int GetDimensions(unsigned short &rows, unsigned short &columns) const;

  ImageIOBase::IOComponentType GetImageDataType() const;
  ImageIOBase::IOPixelType GetImagePixelType() const;

  int GetSpacing(double * const spacing) const;
  int GetOrigin(double * const origin) const;

  bool HasPixelData() const;

  E_TransferSyntax GetTransferSyntax() const;

  long GetFileNumber() const;
  static void
  AddDictEntry(DcmDictEntry *entry);

  static bool CanReadFile(const std::string &filename);
  static bool IsImageFile(const std::string &filename);

private:

  std::string          m_FileName;
  DcmFileFormat*       m_DFile;
  DcmDataset *         m_Dataset;
  E_TransferSyntax     m_Xfer;
  Sint32               m_FrameCount;
  long                 m_FileNumber;
};

extern bool CompareDCMTKFileReaders(DCMTKFileReader *a, DCMTKFileReader *b);
}

#endif // itkDCMTKFileReader_h
